
//
// 130426 - AH - Created.
//

package org.japril.jutil;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFileChooser;
import javax.swing.event.ChangeListener;

/**
 * <p>
 *   Object of help in generating java code.
 * </p>
 * @author Arne Halvorsen (AH)
 */
public final class JavaCodeBuilder
{
  private String m_firstClass=null;
  
  private String m_class=null;
  
  private int m_level=0;
  
  private String m_indent="  ";
  
  private final StringBuilder m_sb=new StringBuilder();
  
  private final CharArrayPrintWriter m_capw=new CharArrayPrintWriter();

  /**
   * <p>
   *   Constructor.
   * </p>
   */
  public JavaCodeBuilder(){}
  
  /**
   * <p>
   *   Sets length of whitespace indentation.
   * </p>
   * @param n Length.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code n<0}.
   */
  public JavaCodeBuilder setIndentationLength(int n)
  {
    if (n<0)
    {
      throw new IllegalArgumentException("n<0");
    }
    
    m_sb.setLength(0);
    for (int i=0; i<n; i++) m_sb.append(' ');
    m_indent=m_sb.toString();
    
    return this;
  }
  
  /**
   * <p>
   *   Makes indentation.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder indentation()
  { 
    m_sb.setLength(0);
    for (int i=0; i<m_level; i++) m_sb.append(m_indent);
    m_capw.print(m_sb);
    return this;
  }
  
  /**
   * <p>
   *   Prints public modifier.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder publicMod(){ return publicMod(true); }
  
  /**
   * <p>
   *   Prints public modifier.
   * </p>
   * @param ind Indent if {@code true} (default).
   * @return {@code this}.
   */
  public JavaCodeBuilder publicMod(boolean ind)
  { 
    if (ind) indentation();
    m_capw.print("public "); return this; 
  }
  
  /**
   * <p>
   *   Prints private modifier.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder privateMod(){ return privateMod(true); }
  
  /**
   * <p>
   *   Prints private modifier.
   * </p>
   * @param ind Indent if {@code true} (default). 
   * @return {@code this}.
   */
  public JavaCodeBuilder privateMod(boolean ind)
  {
    if (ind) indentation();
    m_capw.print("private "); return this; 
  }
  
  /**
   * <p>
   *   Prints static modifier.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder staticMod(){ return staticMod(true); }
  
  /**
   * <p>
   *   Prints static modifier.
   * </p>
   * @param ind Indent if {@code true} (default). 
   * @return {@code this}.
   */
  public JavaCodeBuilder staticMod(boolean ind)
  { 
    if (ind) indentation();
    m_capw.print("static "); 
    return this; 
  }
  
  /**
   * <p>
   *   Prints final modifier.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder finalMod(){ return finalMod(true); }
  
  /**
   * <p>
   *   Prints final modifier.
   * </p>
   * @param ind Indent if {@code true} (default). 
   * @return {@code this}.
   */
  public JavaCodeBuilder finalMod(boolean ind)
  {
    if (ind) indentation();
    m_capw.print("final "); 
    return this; 
  }
  
  /**
   * <p>
   *   Prints protected modifier.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder protectedMod(){ return protectedMod(true); }
  
  /**
   * <p>
   *   Prints protected modifier.
   * </p>
   * @param ind Indent if {@code true} (default). 
   * @return {@code this}.
   */
  public JavaCodeBuilder protectedMod(boolean ind)
  { 
    if (ind) indentation();
    m_capw.print("protected "); 
    return this; 
  }
  
  /**
   * <p>
   *   Generates package statement.
   * </p>
   * @param name Package name. 
   * @return {@code this}.
   */
  public JavaCodeBuilder pack(String name)
  {
    m_sb.setLength(0);
    m_sb.append("package ").append(name).append(";");
    m_capw.println(m_sb);
    return this;
  }
  
  /**
   * <p>
   *   Generates import statements.
   * </p>
   * @param types Types to import.
   * @return {@code this}.
   */
  public JavaCodeBuilder imports(Class<?>... types)
  {
    for (Class<?> curr : types)
    {
      m_sb.setLength(0);
      m_sb.append("import ").append(curr.getName()).append(";");
      m_capw.println(m_sb);
    }
    
    return this;
  }
  
  /**
   * <p>
   *   Append class start.
   * </p>
   * @param name Class name.
   * @return {@code this}.
   */
  public JavaCodeBuilder clazz(String name, Class<?>... xtypes)
  {
    m_sb.setLength(0);
    
    m_class=name;
    
    if (m_firstClass==null) m_firstClass=name;
    
    m_sb.append("class ").append(name);
    
    if (xtypes!=null && xtypes.length>0)
    {
      List<Class<?>> classes=getClasses(xtypes);
      int n=classes.size();
      switch (n)
      {
        case 0 : break;
        case 1 : 
          m_sb.append(" extends ").append(classes.get(0).getSimpleName());
        break;
        default : 
          throw new IllegalArgumentException("can only extend one class");
      }
     
      List<Class<?>> interfaces=getInterfaces(xtypes);
      n=interfaces.size();
      if (n>0)
      {
        m_sb.append(" implements ");
        for (int i=0; i<n; i++)
        {
          if (i>0) m_sb.append(", ");
          m_sb.append(interfaces.get(i).getSimpleName());
        }
      }
    }
    
    m_capw.print(m_sb);
    
    return this;
  }
  
  private List<Class<?>> getInterfaces(Class<?>[] types)
  {
    List<Class<?>> retVal=new ArrayList<Class<?>>();
    
    for (Class<?> curr : types)
    {
      if (curr.isInterface()) retVal.add(curr);
    }
    
    return retVal;
  }
  
  private List<Class<?>> getClasses(Class<?>[] types)
  {
    List<Class<?>> retVal=new ArrayList<Class<?>>();
    
    for (Class<?> curr : types)
    {
      if (!curr.isInterface()) retVal.add(curr);
    }
    
    return retVal;
  }
  
  /**
   * <p>
   * 
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder constructor()
  {
    if (m_class==null)
    {
      throw new IllegalArgumentException("not doing a class it seems!");
    }
    
    m_sb.setLength(0);
    
    m_sb.append(m_class).append("(");
    m_capw.print(m_sb);
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @param name
   * @return {@code this}.
   */
  public JavaCodeBuilder method(String name){ return method(name, null); }
  
  /**
   * <p>
   * </p>
   * @param name
   * @param ret
   * @return {@code this}.
   */
  public JavaCodeBuilder method(String name, Class<?> ret)
  {
    m_sb.setLength(0);
    if (ret==null) m_sb.append("void ");
    else           m_sb.append(ret.getSimpleName()).append(" ");
    
    m_sb.append(name).append("(");
    m_capw.print(m_sb);
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @param type
   * @param name
   * @return {@code this}.
   */
  public JavaCodeBuilder fpar(Class<?> type, String name)
  {
    return par(type, name, true);
  }
  
  /**
   * <p>
   * </p>
   * @param type
   * @param name
   * @return {@code this}.
   */
  public JavaCodeBuilder par(Class<?> type, String name)
  {
    return par(type, name, false);
  }
  
  // Invoked from public par methods.
  private JavaCodeBuilder par(Class<?> type, String name, boolean first)
  {
    m_sb.setLength(0);
    if (!first) m_sb.append(", ");
    m_sb.append(type.getSimpleName()).append(" ").append(name);
    m_capw.print(m_sb);
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder dohtem(){ m_capw.print(")"); return this; }
  
  /**
   * <p>
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder begin()
  {
    m_capw.println();
    indentation();
    m_capw.println("{");
    m_level++;
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder end()
  {
    m_capw.println();
    m_level--;
    indentation();
    m_capw.println("}");
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder beginend()
  {
    m_capw.println();
    indentation();
    m_capw.println("{");
    m_capw.println();
    indentation();
    m_capw.println("}");
    return this;
  }
  
  /**
   * <p>
   *   Clears compiled code so can be reused.
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder clear()
  { 
    m_firstClass=null;
    m_class=null;
    m_capw.clear(); 
    m_level=0; 
    return this; 
  }
  
  public JavaCodeBuilder string(String s){ return string(s, false); }
  
  public JavaCodeBuilder string(String s, boolean ind)
  {
    if (s==null)
    {
      throw new NullPointerException("s");
    }
    
    if (ind) indentation();
    
    m_sb.setLength(0);
    m_sb.append('"');
    int n=s.length();
    for (int i=0; i<n; i++)
    {
      char c=s.charAt(i);
      if (c=='"') m_sb.append("\\").append('"');
      else        m_sb.append(c);
    }
    m_sb.append('"');
    
    m_capw.print(m_sb);
    
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @return {@code this}.
   */
  public JavaCodeBuilder println(){ m_capw.println(); return this; }
  
  /**
   * <p>
   * </p>
   * @param o
   * @return {@code this}.
   */
  public JavaCodeBuilder println(Object o){ return println(o, false); }
  
  /**
   * <p>
   * </p>
   * @param o
   * @return {@code this}.
   */
  public JavaCodeBuilder println(Object o, boolean ind)
  {
    if (o==null)
    {
      throw new NullPointerException("o");
    }
    
    if (ind) indentation();
    m_capw.println(o);
    return this;
  }
  
  /**
   * <p>
   * </p>
   * @param o
   * @return {@code this}.
   */
  public JavaCodeBuilder print(Object o){ return print(o, true); }
  
  /**
   * <p>
   * </p>
   * @param o
   * @return {@code this}.
   */
  public JavaCodeBuilder print(Object o, boolean ind)
  {
    if (o==null)
    {
      throw new NullPointerException("o");
    }
    
    if (ind) indentation();
    m_capw.print(o);
    return this;
  }
  
  @Override
  public String toString(){ return m_capw.toString(); }
  
  /**
   * <p>
   *   Generates java file.
   * </p>
   * @param dir Directory to write file in. Is allowed to generate in directory
   *            which path do not reflect package.
   * @throws IllegalArgumentException If {@code dir} do not specify directory.
   * @throws IllegalStateException If building code has not named outer class.
   * @throws IOException If fails.
   */
  public void generateFile(File dir) throws IOException
  {
    if (dir==null)
    {
      throw new NullPointerException("dir");
    }
    if (!dir.isDirectory())
    {
      throw new NullPointerException("not a directory: "+dir);  
    }
    if (m_firstClass==null)
    {
      throw new IllegalStateException("No class");
    }
    
    File f=new File(dir, m_firstClass+".java");
    IOUtil.save(f, toString());
  }
  
  public static void main(String[] args)
  {
    JavaCodeBuilder jcb=new JavaCodeBuilder();
    jcb.println()
       .pack("org.test")
       .println()
       .imports(JFileChooser.class)
       .println()
       .publicMod().finalMod().clazz("Test", ChangeListener.class)
       .begin()
         .publicMod().constructor().fpar(Integer.TYPE, "capacity").dohtem().beginend()
         .println()
         .protectedMod().method("setName").fpar(String.class, "name").dohtem().beginend()
         .println()
         .publicMod().method("size", Integer.TYPE).dohtem().beginend()
       .end();
    
    System.out.println(jcb);
  }

}
